﻿using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Simplification;

namespace CodeAnalysisRuleCollection.Common.CSharp
{
    [DiagnosticAnalyzer]
    [ExportDiagnosticAnalyzer("LockAnalyzer", LanguageNames.CSharp)]
    internal class LockDiagnosticAnalyzer : ISyntaxNodeAnalyzer<SyntaxKind>
    {
        public static readonly DiagnosticDescriptor RuleForNull = new DiagnosticDescriptor(
            "LockOnNull",
            "Lock on null",
            "Do not lock on null, because it throws a NullReferenceException implicitly",
            "Usage",
            DiagnosticSeverity.Error
        );

        public static readonly DiagnosticDescriptor RuleForString = new DiagnosticDescriptor(
            "LockOnString",
            "Lock on String",
            "Do not lock on strings, because any other code in the process using the same string, will share the same lock",
            "Usage",
            DiagnosticSeverity.None
        );

        public static readonly DiagnosticDescriptor RuleForCurrentInstance = new DiagnosticDescriptor(
            "LockOnCurrentInstance",
            "Lock on current instance",
            "Do not lock on the current instance, because it might be accessed publicly",
            "Usage",
            DiagnosticSeverity.None
        );

        public static readonly DiagnosticDescriptor RuleForTypeOf = new DiagnosticDescriptor(
            "LockOnTypeOf",
            "Lock on a type",
            "Do not lock on a type which is publicly accessible",
            "Usage",
            DiagnosticSeverity.None
        );

        public ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
        {
            get
            {
                return ImmutableArray
                    .Create(RuleForNull)
                    .Add(RuleForString)
                    .Add(RuleForCurrentInstance)
                    .Add(RuleForTypeOf)
                ;
            }
        }

        public ImmutableArray<SyntaxKind> SyntaxKindsOfInterest
        {
            get
            {
                return ImmutableArray.Create(SyntaxKind.LockStatement);
            }
        }

        public void AnalyzeNode(SyntaxNode node, SemanticModel semanticModel, Action<Diagnostic> addDiagnostic, CancellationToken cancellationToken)
        {
            LockStatementSyntax statement = node as LockStatementSyntax;

            // this
            if (statement.Expression is ThisExpressionSyntax || statement.Expression is BaseExpressionSyntax)
            {
                addDiagnostic(Diagnostic.Create(RuleForCurrentInstance, statement.Expression.GetLocation()));
                return;
            }

            // typeof
            else if ((var typeOf = statement.Expression as TypeOfExpressionSyntax) != null)
            {
                if ((var symbol = semanticModel.GetSymbolInfo(typeOf.Type, cancellationToken).Symbol as ITypeSymbol) != null)
                {
                    if (symbol.DeclaredAccessibility == Accessibility.Public || IsAnyParentPubliclyAccessible(symbol))
                    {
                        addDiagnostic(Diagnostic.Create(RuleForTypeOf, statement.Expression.GetLocation()));
                        return;
                    }
                }
            }

            Optional<object> value = semanticModel.GetConstantValue(statement.Expression, cancellationToken);

            if (value.HasValue)
            {
                // null
                if (value.Value == null)
                    addDiagnostic(Diagnostic.Create(RuleForNull, statement.Expression.GetLocation()));

                // string
                else if (value.Value is string)
                    addDiagnostic(Diagnostic.Create(RuleForString, statement.Expression.GetLocation()));
            }
        }

        private static bool IsAnyParentPubliclyAccessible(ITypeSymbol symbol)
        {
            while (symbol != null)
            {
                if (symbol.DeclaredAccessibility == Accessibility.Public || symbol.DeclaredAccessibility == Accessibility.Protected)
                    return true;

                symbol = symbol.ContainingType;
            }

            return false;
        }
    }

    [ExportCodeFixProvider("LockAnalyzer", LanguageNames.CSharp)]
    internal class LockCodeFixProvider : ICodeFixProvider
    {
        public IEnumerable<string> GetFixableDiagnosticIds()
        {
            return new[] {
                LockDiagnosticAnalyzer.RuleForCurrentInstance.Id,
                LockDiagnosticAnalyzer.RuleForNull.Id,
                LockDiagnosticAnalyzer.RuleForString.Id,
                LockDiagnosticAnalyzer.RuleForTypeOf.Id,
                "CS0185"
            };
        }

        public async Task<IEnumerable<CodeAction>> GetFixesAsync(Document document, TextSpan span, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken);

            var diagnostic = diagnostics.First();
            var diagnosticSpan = diagnostic.Location.SourceSpan;

            // Find the local declaration identified by the diagnostic.
            var @lock = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LockStatementSyntax>().First();

            var method = @lock.FirstAncestorOrSelf<BaseMethodDeclarationSyntax>();
            var type = method.FirstAncestorOrSelf<ClassDeclarationSyntax>();

            var model = await document.GetSemanticModelAsync(cancellationToken);

            IMethodSymbol methodSymbol = model.GetDeclaredSymbol(method, cancellationToken);

            if (methodSymbol.IsStatic)
            {
                return new[] {
                    CodeAction.Create("Introduce lock object", c => IntroduceLockObjectAsync(root, document, type, @lock, model, true, c))
                };
            }
            else
            {
                return new[] {
                    CodeAction.Create("Introduce lock object", c => IntroduceLockObjectAsync(root, document, type, @lock, model, false, c)),
                    CodeAction.Create("Introduce lock object (static)", c => IntroduceLockObjectAsync(root, document, type, @lock, model, true, c))
                };
            }
        }

        private async Task<Document> IntroduceLockObjectAsync(
            SyntaxNode root, Document document,
            ClassDeclarationSyntax type, LockStatementSyntax @lock,
            SemanticModel model,
            bool asStatic,
            CancellationToken c
        )
        {
            var newType = type;

            // generate field name
            INamedTypeSymbol typeSymbol = model.GetDeclaredSymbol(type, c);
            string fieldName = IntroducerHelper.GetNextName(typeSymbol, "syncLock");

            // rewrite lock statement to use field to lock on
            newType = type.ReplaceNode(
                @lock,
                @lock.WithExpression(
                    asStatic
                        ? SyntaxFactory.IdentifierName(fieldName) as ExpressionSyntax
                        : SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                            SyntaxFactory.ThisExpression(),
                            SyntaxFactory.IdentifierName(fieldName)
                        )
                            .WithAdditionalAnnotations(Simplifier.Annotation)
                )
            );

            // add lock object field
            newType = CodeGenerator.AddFieldDeclaration(
                newType,
                CodeGenerationSymbolFactory.CreateFieldSymbol(
                    null,
                    Accessibility.Private,
                    new SymbolModifiers(isStatic: asStatic),
                    model.Compilation.GetSpecialType(SpecialType.System_Object),
                    fieldName,
                    initializer: SyntaxFactory.ObjectCreationExpression(
                        SyntaxFactory.ParseTypeName("object"),
                        SyntaxFactory.ArgumentList(),
                        null
                    )
                ),
                document.Project.Solution.Workspace
            );

            var newRoot = root.ReplaceNode(type, newType);

            return document.WithSyntaxRoot(newRoot);
        }
    }
}
